/*
 * vim: ts=4 sw=4 fdm=marker noet tw=78 ai
 * %%LICENSE%%
 */
#include <sys/cdefs.h>
#ifdef __FBSDID
__FBSDID("$Coar$");
#endif

#include <stdlib.h>
#include <stdio.h>
#include <stdbool.h>

#include <assert.h>
#include <err.h>

#include <lber.h>
#include <ldif.h>
#include <ldap.h>

#include "extern.h"
#include "modify.h"

/*
 * ldif_read_record simply reads an ldif file into a buf. It is essentially
 * useless to us. It's primary advantage is that it can read multiple files
 * which are chained using LDIF "include:" types. We don't support multiple
 * files.
 *
 * Lines need to be parsed with ldif_parse_line. As such, it's easier to
 * open the file using stdio and suck in the file ourselves. We do need a
 * buffer, because ldif_getline works on buffers and folds continious lines
 * for us.
 * NOTE:
 * Both ldif_getline and ldif_parse_line2 operate on the buffer provided in
 * place. This means that we do NOT do intermediate free() operations and it
 * also means we have to duplicate the results.
 *
 * Since a file (and it's buffer) can have multiple DN's and the DN is not
 * part of the LDAPMod structure, we will have to bundle the modifications per
 * DN and update the buffer position to the start of the next DN. This also
 * means we have to save the pointer ldif_getline gives us, before handing it
 * to ldif_parse_line.
 *
 * It is our job to interpret the semantics of the ldif line:
 * - dn: Set the dn in the current mod.
 * - changetype:
 *   - modify: do nothing special
 *   - add: there will be no additional instructions, everything is an
 *     LDAP_MOD_ADD operation and each line will be a record, till the next dn
 *     is encountered.
 *   - moddn: __RESEARCH__
 *   - modrdn: __RESEARCH__
 * - add: set LDAP_MOD_ADD in the current mod
 * - delete: set LDAP_MOD_DELETE in the current mod
 * - "-": create a new mod
 *
 * There is also LDAP_MOD_REPLACE, though I'm not sure how to use it. The
 * LDAPMod structure doesn't provide an "old value", "new value" storage and
 * ldif specifically uses this construct so that a server can verify if the
 * old value is what it has in it's database. If it's not a server will deny
 * the modification.
 */

#define EC_OK			0
#define EC_ALLOC_ERR	1
#define EC_PARSE_ERR	2
#define EC_NEED_DN		3

/*
 * Arbitrary number. One should be sufficient to make the entire operation
 * fail, yet we should be somewhat forgiving of editor inserted garbage.
 */
#ifndef LDIF_MAX_PARSE_ERRORS
#define LDIF_MAX_PARSE_ERRORS	3
#endif

#define LDE_DIAG(__ctx, __msg) \
	(void)ldap_set_option((__ctx)->ld, LDAP_OPT_DIAGNOSTIC_MESSAGE, __msg)

#define ATTR_IS_DN(__attr) ( strcmp((__attr), "dn") == 0 )

size_t
lde_parse_mods(
		runtime_t *ctx, // Context
		char **bufp, // Pointer to the buffer
		char **dnp, // Pointer to a buffer filled with the DN
		LDAPMod ***modsp, // Pointer to an array with modifications
		bool is_add, // Whether all records are additions
		const char **errmsgp // Pointer to local storage error message
		)
{
	LDAPMod			**mods; // Array of modifications
	size_t			  nmods; // Number of modifications so far
	LDAPMod			 *cur; // Current modification
	//int				  res; // Results of operations
	char			 *buf = *bufp; // File buffer
	char			 *line; // Line buffer
	char			 *line_start; // Start of line buffer before parsing
	char			 *dn; // Our dn
	int				  ec; // Error code
	int				  nperr; // Number of parse errors
	const char *errvals[] = {
		NULL,
		"Allocation error",
		"Parse error",
		"Expected DN"
	};

	assert(ctx != NULL);
	assert(bufp != NULL && *bufp != NULL);
	TRACEIN(ctx, __func__, "Parsing %p", *bufp);

	nmods = 0;
	ec = EC_OK;
	dn = NULL;
	mods = NULL;
	nperr = 0;

	if( (cur = malloc(sizeof(*cur))) == NULL )
	{
		ec = EC_ALLOC_ERR;
		ldap_set_option(ctx->ld, LDAP_OPT_DIAGNOSTIC_MESSAGE,
				"Allocation of LDAPMod structure failed.");
		goto out;
	}

	cur->mod_type = NULL;
	while( (line = ldif_getline(&buf)) != NULL )
	{
		int is_bin, new_mod;
		struct berval attr, value;

		line_start = line;
		if( ldif_parse_line2(line, &attr, &value, &is_bin) != 0 )
		{
			if( nperr++ < LDIF_MAX_PARSE_ERRORS )
				continue;
			else
			{
				ec = EC_PARSE_ERR;
				LDE_DIAG(ctx, line_start);
				goto out;
			}
		}
		if( dn == NULL && ! ATTR_IS_DN(attr.bv_val) )
		{
			ec = EC_NEED_DN;
			LDE_DIAG(ctx, attr.bv_val);
			goto out;
		}

		if( new_mod )
		{
			// nmods+2 as we allocate the NULL terminator each time as well
			if( (mods = reallocf(mods, (nmods+2) * sizeof(*mods))) == NULL )
			{
				ec = EC_ALLOC_ERR;
				LDE_DIAG(ctx, "Failed to allocate modifcation array");
				goto out;
			}
			mods[nmods] = cur;
			nmods += 1;
			if( (cur = malloc(sizeof(*cur))) == NULL )
			{
				ec = EC_ALLOC_ERR;
				LDE_DIAG(ctx, "Failed to allocate a new modification");
				goto out;
			}
		}
	}

out:
	if( mods != NULL ) mods[nmods] = NULL;
	if( *errmsgp ) *errmsgp = errvals[ec];

	*modsp = mods;
	TRACEOUT(ctx, __func__, "Returning %zu with err code %i", nmods, ec);
	return (nmods);
}
